metaclasses
可以說是Python最難掌握的範疇。如果是第一次接觸這些概念,很容易出現我是誰?
,我在幹麻?
,我要去哪裡?
的徵狀。如果出現類似的情形,請不要驚慌,這是非常正常的XD
在日常應用中,直接使用metaclasses
的機會不高,但了解metaclasses
能讓我們由另一個視角,來欣賞Python的優雅。
下面引用Python大神,Tim Peters
(註1
),對於metaclasses
的描述:
“Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why).”
— Tim Peters
instance
的initialize
及instantiate
。class
的生成過程。__call__
的細節。metaclasses
相關知識。Python的class
是繼承object
而來,所以
class MyClass:
pass
是等義於
class MyClass(object):
pass
如果help(object)
,可以看到其內建有__init__
(instance method
)及__new__
(static method
)。
| __init__(self, /, *args, **kwargs)
| Initialize self. See help(type(self)) for accurate signature.
| Static methods defined here:
|
| __new__(*args, **kwargs) from builtins.type
| Create and return a new object. See help(type) for accurate signature.
initialize
是指instance
已經建立,而我們要進一步初始化,一般為呼叫__init__
。
instantiate
則是指使用__new__
建立instance
。
__init__
# 01
中,建立了客製化的__init__
(即overwrite了object.__init__
),使得透過MyClass
建立的instance
於初始化時,可以指定instance variable
self.x
的值。
# 01
class MyClass:
def __init__(self, x):
self.x = x
if __name__ == '__main__':
my_inst = MyClass(1)
print(my_inst.__dict__) # {'x': 1}
__init__
是個instance method
,而instance method
的第一個參數,一般約定俗成地取名為self
。既然self
能夠被傳入__init__
,代表instance
本身是在__init__
被呼叫前即已建立。
__new__
事實上,object.__new__
才是Python真正建立instance
時所呼叫的。
# 02
中,建立了客製化的__new__
(即overwrite了object.__new__
)。
__new__
的第一個參數為cls
,在# 02
中即為MyClass
,至於其它參數則需要與__init__
一致(如果有的話)。__new__
中呼叫super().__new__(cls)
來建立instance
,在# 02
中呼叫super().__new__(cls)
相當於呼叫object.__new__(cls)
,但習慣使用super()
的寫法,可以幫助我們在繼承的時候,減少一些問題(註2
)。__new__
回傳一個MyClass
的instance
時(註3
),會自動呼叫__init__
,我們可以id
來確認__new__
中的instance
就是傳入__init__
中的self
。# 02
class MyClass:
def __new__(cls, x):
instance = super().__new__(cls)
print(f'{id(instance)=}')
return instance
def __init__(self, x):
print(f'{id(self)=}')
self.x = x
if __name__ == '__main__':
my_inst = MyClass(1)
print(my_inst.__dict__) # {'x': 1}
理論上,任何在__init__
中能做的事,都能夠在__new__
中完成。
# 03
中,我們將__init__
中指定的instance variable
self.x
搬到__new__
中,如此則可免去建立__init__
。
# 03
class MyClass:
def __new__(cls, x):
instance = super().__new__(cls)
instance.x = x
return instance
if __name__ == '__main__':
my_inst = MyClass(1)
print(my_inst.__dict__) # {'x': 1}
如果對於理解# 03
有困難的朋友,或許可以將instance
想為self
,改寫為# 04
。
# 04
class MyClass:
def __new__(cls, x):
self = super().__new__(cls)
self.x = x
return self
if __name__ == '__main__':
my_inst = MyClass(1)
print(my_inst.__dict__) # {'x': 1}
假設現在有個SlowNewClass
,而其__new__
有很多操作,需時良久(以time.sleep(1)
表示)。
# 05
import time
class SlowNewClass:
def __new__(cls, **kwargs):
time.sleep(1)
return super().__new__(cls)
def __init__(self, **kwargs):
self.__dict__.update(**kwargs)
...
題目要求:
class
,且必須繼承SlowNewClass
。class
僅接受**kwargs
參數,且kwargs
內所有value
必須滿足value>=0
,否則raise ValueError
。若kwargs
中有x
變數,需要將其從kwargs
移出,進行某些操作(以self.x = x+100
表示),再將剩下的kwargs
利用super().__init__
往上傳遞。於# 05
中,我們建立了MyClass
與MyClass2
,並利用timer
來觀察instance
的生成速度。
# 05
...
def timer(cls, **kwargs):
try:
start = time.perf_counter()
my_inst = cls(**kwargs)
except ValueError:
pass
finally:
end = time.perf_counter()
elapsed = end - start
print(f'{elapsed=:.6f} secs for {cls}')
class MyClass(SlowNewClass):
def __init__(self, **kwargs):
if all(value >= 0 for value in kwargs.values()):
if x := kwargs.pop('x', None):
self.x = x+100
super().__init__(**kwargs)
else:
raise ValueError
class MyClass2(SlowNewClass):
def __new__(cls, **kwargs):
if all(value >= 0 for value in kwargs.values()):
return super().__new__(cls, **kwargs)
raise ValueError
def __init__(self, **kwargs):
if x := kwargs.pop('x', None):
self.x = x+100
super().__init__(**kwargs)
if __name__ == '__main__':
my_inst = MyClass(x=1, y=2)
print(my_inst.__dict__) # {'x': 101, 'y': 2}
my_inst2 = MyClass2(x=1, y=2)
print(my_inst2.__dict__) # {'x': 101, 'y': 2}
print('normal: ')
timer(MyClass, x=1, y=2) # 1.000700
timer(MyClass2, x=1, y=2) # 1.000952
print('exceptions: ')
timer(MyClass, x=-1, y=2) # 1.000298
timer(MyClass2, x=-1, y=2) # 0.000011
MyClass
,於__init__
中進行操作。但是這有一個缺點是速度很慢,因為我們必須繼承SlowNewClass
,透過其耗時的__new__
來生成instance
。換句話說,即使我們很快就判斷出需要raise ValueError
,我們還是得等待SlowNewClass.__new__
生成instance
後才可操作。MyClass2
同時實作__new__
及__init__
。這樣一來,我們可以於__new__
呼叫super()__new__
前,就先決定是否要raise ValueError
,然後將後續需要呼叫super().__init__
的工作放到__init__
。MyClass
與MyClass2
速度差不多,但是在有例外的情況下,MyClass2
可以馬上raise
。由於__new__
的第一個參數是cls
,所以除了能指定instance variable
外,也能對MyClass
做一些手腳,例如插入一個instance method
hi
。
# 06
class MyClass:
def __new__(cls, x: int):
cls.hi = lambda self: 'hi'
instance = super().__new__(cls)
instance.x = x
return instance
if __name__ == '__main__':
my_inst = MyClass(1)
print(my_inst.hi()) # hi
但是這麼寫有點「微妙」,因為這相當於在每次生成instance
時,都會mutate
cls
一次,建立一個新的instance method
hi
。
# 07
嘗試繼承str
,並攔截給定字串,於其後加上_123
。
# 07
class MyStr1(str):
def __init__(self, s):
super().__init__(s + '_123')
class MyStr2(str):
def __new__(cls, s):
return super().__new__(cls, s + '_123')
if __name__ == '__main__':
# my_str1 = MyStr1('abc') # TypeError
my_str2 = MyStr2('abc')
print(my_str2) # 'abc_123'
MyStr1
中,super().__init__
會raise TypeError
。MyStr2
中,super().__new__
,則可以成功得到abc_123
。class
中實作__new__
?__init__
之前,於建立instance
前後做一些操作時(實例說明1
)。__new__
中操控cls
,暗指每次生成instance
時,都會mutate
cls
,需謹慎考慮這是否為您想要的行為(實例說明2
)。C
實作的built-in
type
時。註1:Tim Peters
也是著名Zen of Python
的作者。什麼?您沒聽過嗎?那麼請打開Python的repl,輸入import this
好好欣賞一下吧。
註2:如果不太熟悉super()
的朋友,可以參考這篇由Raymond Hettinger
所寫的介紹。
註3:如果__new__
所回傳的並非cls
的instance
,則不會自動呼叫__init__
。但儘管如此,我們可以手動呼叫__init__
,所以可以寫出如# 101
的code
。
# 101
from types import SimpleNamespace
class MyClass:
def __new__(cls, x):
return SimpleNamespace()
def __init__(self, x):
self.x = x
def hi(self):
return 'hi'
if __name__ == '__main__':
my_inst = MyClass(1) # __init__ not being called
print(type(my_inst)) # <class 'types.SimpleNamespace'>
MyClass.__init__(my_inst, 1)
print(my_inst.__dict__) # {'x': 1}
print(MyClass.hi(my_inst)) # hi